﻿using System;
using System.Collections;
using System.Collections.Specialized;
using System.DirectoryServices.Protocols;
using System.Globalization;
using System.Collections.Generic;

namespace ActiveDirectoryUtilities.DirectoryServiceAccess
{
	public enum LDAP_PORTS
	{
		LDAP = 389,
		Secure_LDAP = 636,
		Global_Catalog = 3268,
		Secure_Global_Catalog = 3269
	}

	/// <summary>
	/// http://msdn.microsoft.com/en-us/library/cc223254(PROT.10).aspx
	/// </summary>
	public class RootDse
	{
		private enum attributes
		{
			configurationNamingContext,

			/// <summary>
			/// UTC Format
			/// </summary>
			currentTime,
			defaultNamingContext,
			dnsHostName,
			domainControllerFunctionality,
			domainFunctionality,
			dsServiceName,
			forestFunctionality,
			isGlobalCatalogReady,
			isSynchronized,
			ldapServiceName,
			highestCommittedUSN,

			/// <summary>
			/// Multi-Valued String
			/// </summary>
			namingContexts,
			rootDomainNamingContext,
			schemaNamingContext,
			serverName,
			subschemaSubentry,

			/// <summary>
			/// Multi-Valued String
			/// </summary>
			supportedCapabilities,	//Multi-Valued

			/// <summary>
			/// Multi-Valued String
			/// </summary>
			supportedControl,	//Multi-Valued

			/// <summary>
			/// Multi-Valued String
			/// </summary>
			supportedLDAPPolicies,	//Multi-Valued

			/// <summary>
			/// Multi-Valued String
			/// </summary>
			supportedLDAPVersion,	//Multi-Valued

			/// <summary>
			/// Multi-Valued String
			/// </summary>
			supportedSASLMechanisms	//Multi-Valued?
		}

		private const string BASEDN = null;
		private const string SEARCH_FILTER = "(objectClass=*)";
		private const SearchScope SCOPE = SearchScope.Base;

		private SearchResultEntry _rootDse;

		public string ConfigurationNamingContext
		{ get { return _rootDse.Attributes[attributes.configurationNamingContext.ToString()][0].ToString(); } }

		public DateTime CurrentTime
		{
			get
			{
				string dateTime = _rootDse.Attributes[attributes.currentTime.ToString()][0].ToString();
				int year = int.Parse(dateTime.Substring(0, 4), NumberFormatInfo.InvariantInfo);
				int month = int.Parse(dateTime.Substring(4, 2), NumberFormatInfo.InvariantInfo);
				int day = int.Parse(dateTime.Substring(6, 2), NumberFormatInfo.InvariantInfo);
				int hour = int.Parse(dateTime.Substring(8, 2), NumberFormatInfo.InvariantInfo);
				int minute = int.Parse(dateTime.Substring(10, 2), NumberFormatInfo.InvariantInfo);
				return new DateTime(year, month, day, hour, minute, int.Parse(dateTime.Substring(12, 2), NumberFormatInfo.InvariantInfo), 0);
			}
		}

		public string DefaultNamingContext
		{ get { return _rootDse.Attributes[attributes.defaultNamingContext.ToString()][0].ToString(); } }

		public string DnsHostName
		{ get { return _rootDse.Attributes[attributes.dnsHostName.ToString()][0].ToString(); } }

		/// <summary>
		/// http://msdn.microsoft.com/en-us/library/cc223272(PROT.10).aspx
		/// </summary>
		public DomainControllerMode DomainControllerFunctionality
		{
			get
			{
				if (_rootDse.Attributes.Contains(attributes.domainFunctionality.ToString()))
				{
					return (DomainControllerMode)int.Parse(_rootDse.Attributes[attributes.domainControllerFunctionality.ToString()][0].ToString());
				}
				return DomainControllerMode.Windows_2000;
			}
		}

		/// <summary>
		/// http://msdn.microsoft.com/en-us/library/cc223273(PROT.10).aspx
		/// </summary>
		public DomainMode DomainFunctionality
		{
			get
			{
				if (_rootDse.Attributes.Contains(attributes.domainFunctionality.ToString()))
				{
					return (DomainMode)int.Parse(_rootDse.Attributes[attributes.domainFunctionality.ToString()][0].ToString());
				}
				
				return DomainMode.Windows_2000;
			}
		}

		public string DsServiceName
		{ get { return _rootDse.Attributes[attributes.dsServiceName.ToString()][0].ToString(); } }

		/// <summary>
		/// http://msdn.microsoft.com/en-us/library/cc223274(PROT.10).aspx
		/// </summary>
		public ForestMode ForestFunctionality
		{
			get
			{
				if (_rootDse.Attributes.Contains(attributes.forestFunctionality.ToString()))
				{
					return (ForestMode)int.Parse(_rootDse.Attributes[attributes.forestFunctionality.ToString()][0].ToString());
				}
				return ForestMode.Windows_2000;
			}
		}

		public long HighestCommittedUSN
		{ get { return long.Parse(_rootDse.Attributes[attributes.highestCommittedUSN.ToString()][0].ToString()); } }

		public bool IsGlobalCatalogReady
		{ get { return bool.Parse(_rootDse.Attributes[attributes.isGlobalCatalogReady.ToString()][0].ToString()); } }

		public bool IsSynchronized
		{ get { return bool.Parse(_rootDse.Attributes[attributes.isSynchronized.ToString()][0].ToString()); } }

		public string LdapServiceName
		{ get { return _rootDse.Attributes[attributes.ldapServiceName.ToString()][0].ToString(); } }

		public string[] NamingContexts
		{
			get
			{
				int arraySize = _rootDse.Attributes[attributes.namingContexts.ToString()].Count;
				string[] namingContexts = new string[arraySize];
				for (int i = 0; i < arraySize; i++)
				{
					namingContexts[i]
						= (string)_rootDse.Attributes[attributes.namingContexts.ToString()][i];
				}
				return namingContexts;
			}
		}

		public string RootDomainNamingContext
		{ get { return _rootDse.Attributes[attributes.rootDomainNamingContext.ToString()][0].ToString(); } }

		public string SchemaNamingContext
		{ get { return _rootDse.Attributes[attributes.schemaNamingContext.ToString()][0].ToString(); } }

		public string ServerName
		{ get { return _rootDse.Attributes[attributes.serverName.ToString()][0].ToString(); } }

		public string SubschemaSubentryToString
		{ get { return _rootDse.Attributes[attributes.subschemaSubentry.ToString()][0].ToString(); } }

		public string[] SupportedCapabilities
		{
			get
			{
				int arraySize = _rootDse.Attributes[attributes.supportedCapabilities.ToString()].Count;
				string[] supportedCapabilities = new string[arraySize];
				for (int i = 0; i < arraySize; i++)
				{
					supportedCapabilities[i]
						= (string)_rootDse.Attributes[attributes.supportedCapabilities.ToString()][i];
				}
				return supportedCapabilities;
			}
		}

		public string[] SupportedControl
		{
			get
			{
				int arraySize = _rootDse.Attributes[attributes.supportedControl.ToString()].Count;
				string[] supportedControl = new string[arraySize];
				for (int i = 0; i < arraySize; i++)
				{
					supportedControl[i]
						= (string)_rootDse.Attributes[attributes.supportedControl.ToString()][i];
				}
				return supportedControl;
			}
		}

		public string[] SupportedLDAPPolicies
		{
			get
			{
				int arraySize = _rootDse.Attributes[attributes.supportedLDAPPolicies.ToString()].Count;
				string[] supportedLDAPPolicies = new string[arraySize];
				for (int i = 0; i < arraySize; i++)
				{
					supportedLDAPPolicies[i]
						= (string)_rootDse.Attributes[attributes.supportedLDAPPolicies.ToString()][i];
				}
				return supportedLDAPPolicies;
			}
		}

		public string[] SupportedLDAPVersion
		{
			get
			{
				int arraySize = _rootDse.Attributes[attributes.supportedLDAPVersion.ToString()].Count;
				string[] supportedLDAPVersion = new string[arraySize];
				for (int i = 0; i < arraySize; i++)
				{
					supportedLDAPVersion[i]
						= (string)_rootDse.Attributes[attributes.supportedLDAPVersion.ToString()][i];
				}
				return supportedLDAPVersion;
			}
		}

		public string[] SupportedSASLMechanismsToString
		{
			get
			{
				int arraySize = _rootDse.Attributes[attributes.supportedSASLMechanisms.ToString()].Count;
				string[] supportedSASLMechanismsToString = new string[arraySize];
				for (int i = 0; i < arraySize; i++)
				{
					supportedSASLMechanismsToString[i]
						= (string)_rootDse.Attributes[attributes.supportedSASLMechanisms.ToString()][i];
				}
				return supportedSASLMechanismsToString;
			}
		}

		public RootDse()
			: this(string.Empty, (int)LDAP_PORTS.LDAP)
		{
		}

		public RootDse(string connnectionPoint)
			: this(connnectionPoint, (int)LDAP_PORTS.LDAP)
		{
		}

		public RootDse(string connectionPoint, int portNumber)
		{
			using (WLdapSearchRequest wlsr =
				new WLdapSearchRequest(connectionPoint, portNumber, BASEDN, SEARCH_FILTER, SCOPE))
			{
				_rootDse = wlsr.FindOne();
			}
		}
	}

	public class WLdapSearchRequest : IDisposable
	{
		#region Constants
		const string DEFAULT_FILTER = "(objectClass=*)";
		const int DEFAULT_SIZE_LIMIT = 1000;	//0=use server page size
		const int DEFAULT_TIME_LIMIT = -1;	//Can't use a TimeSpan const, so this many seconds in constructor
		const SearchScope DEFAULT_SEARCH_SCOPE = SearchScope.Base;
		private static readonly TimeSpan DEFAULT_TIMESPAN;
		#endregion

		private bool _disposed;
		private SearchRequest _request;
		private LdapConnection _ldapconn;

		#region Expose configurable settings for SearchRequest
		public StringCollection Attributes
		{
			get { return _request.Attributes; }
			//set { _request.Attributes = value; }
		}

		public DirectoryControlCollection Controls
		{
			get { return _request.Controls; }
			//set { _request.Controls = value; }
		}

		public string SearchRootDistinguishedName
		{
			get { return _request.DistinguishedName; }
			set { _request.DistinguishedName = value; }
		}

		public string Filter
		{
			get { return _request.Filter.ToString(); }
			set { _request.Filter = value; }
		}

		public SearchScope Scope
		{
			get { return _request.Scope; }
			set { _request.Scope = value; }
		}

		public int SizeLimit
		{
			get { return _request.SizeLimit; }
			set { _request.SizeLimit = value; }
		}

		public TimeSpan LdapSearchServerTimeOut
		{
			get { return _request.TimeLimit; }
			set { _request.TimeLimit = value; }
		}

		public TimeSpan LdapSearchClientTimeOut
		{
			get { return _ldapconn.Timeout; }
			set { _ldapconn.Timeout = value; }
		}

		public bool TypesOnly
		{
			get { return _request.TypesOnly; }
			set { _request.TypesOnly = value; }
		}

		#endregion

		#region c'tors
		static WLdapSearchRequest()
		{
			DEFAULT_TIMESPAN = new TimeSpan(0, 0, DEFAULT_TIME_LIMIT);
		}

		public WLdapSearchRequest(string connectionPoint, int portNumber)
			: this(connectionPoint, portNumber, null, DEFAULT_FILTER, DEFAULT_SEARCH_SCOPE, null)
		{
		}

		public WLdapSearchRequest(string connectionPoint, int portNumber, string searchRoot)
			: this(connectionPoint, portNumber, searchRoot, DEFAULT_FILTER, DEFAULT_SEARCH_SCOPE, null)
		{
		}

		public WLdapSearchRequest(string connectionPoint, int portNumber, string searchRoot, string filter)
			: this(connectionPoint, portNumber, searchRoot, filter, DEFAULT_SEARCH_SCOPE, null)
		{
		}

		public WLdapSearchRequest(string connectionPoint, int portNumber, string searchRoot, string filter, SearchScope scope)
			: this(connectionPoint, portNumber, searchRoot, filter, scope, null)
		{
		}

		public WLdapSearchRequest(string connectionPoint, int portNumber, string searchRoot, string filter, SearchScope scope, string[] attributeList)
		{
			System.Diagnostics.Debug.WriteLine("Connection Point:\t" + connectionPoint
			+ "\nPort Number:\t" + portNumber.ToString()
			+ "\nSearch Root:\t" + searchRoot
			+ "\nLDAP Filter:\t" + filter
			+ "\nSearch Scope:\t" + scope.ToString()
			+ "\nNum Attributes:\t" + (attributeList != null ? attributeList.Length.ToString() : "NULL"));

			#region Establish a connection
			LdapDirectoryIdentifier ldi = new LdapDirectoryIdentifier(connectionPoint, portNumber, false, false);
			_ldapconn = new LdapConnection(ldi);
			_ldapconn.SessionOptions.ProtocolVersion = 3;
			_ldapconn.SessionOptions.ReferralChasing = ReferralChasingOptions.None;
			_ldapconn.Timeout = new TimeSpan(0, 0, 300);
			#endregion

			#region Formulate the search request
			_request = new SearchRequest(searchRoot, filter, scope, attributeList);
			_request.SizeLimit = DEFAULT_SIZE_LIMIT;

			//Submit paged search
			PageResultRequestControl pageControl = new PageResultRequestControl(_request.SizeLimit);
			_request.Controls.Add(pageControl);
			//DirectoryControl statsControl = new DirectoryControl("1.2.840.113556.1.4.970", null, false, true);
			ServerGetStatsControl statsControl = new ServerGetStatsControl(); //((new RootDse()).DomainControllerFunctionality);
			_request.Controls.Add(statsControl);
			#endregion
		}
		#endregion

		public WLdapSearchResultCollection FindAll()
		{
			return new WLdapSearchResultCollection(_ldapconn, _request);
		}

		public SearchResultEntry FindOne()
		{
			//this._request.SizeLimit = 1;
			WLdapSearchResultCollection wsrc = this.FindAll();
			foreach (SearchResultEntry sre in wsrc)
			{
				System.Diagnostics.Debug.WriteLine("Search Root:\t" + (!string.IsNullOrEmpty(_request.DistinguishedName) ? _request.DistinguishedName : "RootDSE")
				+ "\nLDAP Filter:\t" + _request.Filter
				+ "\nSearch Scope:\t" + _request.Scope.ToString()
				+ "\nNum Attributes:\t" + (_request.Attributes != null ? _request.Attributes.Count.ToString() : "NULL")
				+ "\nNumber Of Waits on Server:  " + wsrc.NumberOfWaitsOnResponseFromServer.ToString("N0")
				+ "\nSeconds Waiting on Server:  " + wsrc.TimeSpentWaitingOnServerResponse.TotalSeconds.ToString("N0")
				+ "." + wsrc.TimeSpentWaitingOnServerResponse.Milliseconds.ToString("N0"));
				return sre;
			}
			return null;
		}

		#region IDisposable Members

		public void Dispose()
		{
			this.Dispose(true);
		}

		protected void Dispose(bool disposing)
		{
			if (!_disposed)
			{
				if (disposing)
				{
					_ldapconn.Dispose();
				}
			}
			_disposed = true;
		}
		#endregion
	}

	public class WLdapSearchResultCollection : IEnumerable, IEnumerator<SearchResultEntry>
	{
		private LdapConnection _ldapconn;
		private SearchRequest _request;
		private SearchResponse _response;
		private PageResultRequestControl _pageControl;
		private int _currentIndex = -1;
		private int _numberOfWaitsOnResponseFromServer = 0, _ticksWaitingOnEntriesFromServer = 0;

		public TimeSpan TimeSpentWaitingOnServerResponse { get { return new TimeSpan(0, 0, 0, 0, _ticksWaitingOnEntriesFromServer); } }
		public int NumberOfWaitsOnResponseFromServer { get { return _numberOfWaitsOnResponseFromServer; } }

		internal WLdapSearchResultCollection(LdapConnection ldapconn, SearchRequest request)
		{
			_ldapconn = ldapconn;
			_request = request;

			foreach (DirectoryControl control in _request.Controls)
			{

				if (control is PageResultRequestControl)
				{
					_pageControl = (PageResultRequestControl)control;
					break;
				}
				else
				{
					System.Diagnostics.Debug.WriteLine(control.Type.ToString());
				}
			}

			int startTicks = Environment.TickCount;
			_response = (SearchResponse)_ldapconn.SendRequest(_request);
			_currentIndex = -1;
			_ticksWaitingOnEntriesFromServer += Environment.TickCount - startTicks;
			_numberOfWaitsOnResponseFromServer++;
		}

		private bool getNextPage()
		{
			foreach (DirectoryControl control in _response.Controls)
			{
				if (control.Type == ServerGetStatsControl.Type)
				{
					ServerGetStatsControl stats = new ServerGetStatsControl(control);
				}
			}

			//This will only return true if this is the end of page and a new one is loaded.
			//	Warning, the page could be empty
			foreach (DirectoryControl control in _response.Controls)
			{
				if (control is PageResultResponseControl)
				{
					if (((PageResultResponseControl)control).Cookie.Length != 0)
					{
						_pageControl.Cookie = ((PageResultResponseControl)control).Cookie;
						int startTicks = Environment.TickCount;
						_response = (SearchResponse)_ldapconn.SendRequest(_request);
						_currentIndex = 0;
						_ticksWaitingOnEntriesFromServer += Environment.TickCount - startTicks;
						_numberOfWaitsOnResponseFromServer++;
						return _response.Entries.Count > 0;
					}
				}
				else
				{
					System.Diagnostics.Debug.WriteLine(control.Type.ToString());
				}
			}
			TimeSpan runtime = new TimeSpan(0, 0, 0, 0, _ticksWaitingOnEntriesFromServer);
			System.Diagnostics.Debug.WriteLine("Search Root:\t" + (!string.IsNullOrEmpty(_request.DistinguishedName) ? _request.DistinguishedName : "RootDSE")
			+ "\nLDAP Filter:\t" + _request.Filter
			+ "\nSearch Scope:\t" + _request.Scope.ToString()
			+ "\nNum Attributes:\t" + (_request.Attributes != null ? _request.Attributes.Count.ToString() : "NULL")
			+ "\nNumber Of Waits on Server:  " + _numberOfWaitsOnResponseFromServer.ToString("N0")
			+ "\nSeconds Waiting on Server:  " + runtime.TotalSeconds.ToString("N0")
			+ "." + runtime.Milliseconds.ToString("N0"));
			return false;
		}

		#region IEnumerable Members

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			return this;
		}
		#endregion

		#region IEnumerator Members

		object IEnumerator.Current
		{
			get { return _response.Entries[_currentIndex]; }
		}

		public bool MoveNext()
		{
			if (++_currentIndex == _response.Entries.Count)
			{
				return getNextPage();
			}
			return true;
		}

		public void Reset()
		{
			_currentIndex = -1;
			_request.Controls.Clear();
			_response = (SearchResponse)_ldapconn.SendRequest(_request);
		}

		#endregion

		#region IEnumerator<SearchResultEntry> Members

		public SearchResultEntry Current
		{
			get { return _response.Entries[_currentIndex]; }
		}

		#endregion

		#region IDisposable Members

		public void Dispose()
		{
			_ldapconn.Dispose();
		}

		#endregion
	}

	/// <summary>
	/// Manage behavior of Stats control
	/// http://msdn.microsoft.com/en-us/library/cc200562.aspx
	/// </summary>
	[Flags]
	public enum ServerGetStatsControlOptions
	{
		/// <summary>
		/// Perform the search as if no LDAP_SERVER_GET_STATS_OID control was included in the search request.
		/// </summary>
		NORMAL = 0,
		/// <summary>
		/// Perform the search and return data related to the resources consumed performing the search, as well as the actual search results.
		/// </summary>
		STATS = 1,
		/// <summary>
		/// Return data related to how the search would be performed, but do not actually return the search results.
		/// </summary>
		ONLY_OPTIMIZE = 2,
		/// <summary>
		/// Windows Server 2008: Returns the data in an alternative format documented later in this section.
		///<para>Windows 2000 and Windows Server 2003: Not supported.</para>
		/// </summary>
		EXTENDED_FMT = 4
	}

	/// <summary>
	/// Return query statistics as per LDAP_SERVER_GET_STATS_OID
	/// <para>http://msdn.microsoft.com/en-us/library/cc200562.aspx</para>
	/// </summary>
	public class ServerGetStatsControl : DirectoryControl
	{
		public ServerGetStatsControl(DirectoryControl stats)
			: base(stats.Type, stats.GetValue(), stats.IsCritical, stats.ServerSide)
		{
		}

		private ServerGetStatsControl(string type, byte[] value, bool isCritical, bool serverSide)
			: base(type, value, isCritical, serverSide)
		{
		}

		public ServerGetStatsControl(ServerGetStatsControlOptions option)
			: base("1.2.840.113556.1.4.970", null, false, true)
		{
		}

		public ServerGetStatsControl()
			: this(ServerGetStatsControlOptions.STATS)
		{
		}

		new static public string Type { get { return "1.2.840.113556.1.4.970"; } }

		public override byte[] GetValue()
		{
			return base.GetValue();
			
			//Console.WriteLine(value.Length);
			//object[] directoryControlValue;
			//if (value.Length == 0) return null;
			//directoryControlValue = BerConverter.Decode("{iiiiiiiiiaiaiiiiiiiiiiiiii}", value);	//Win 2008
			////directoryControlValue = BerConverter.Decode("{iiiiiiiiiaia}", value);	//Win 2003
			////directoryControlValue = BerConverter.Decode("{iiiiiiii}", value);	//Win 2000

			//Console.WriteLine(directoryControlValue.Length);

			//return null;

			//if (directoryControlValue == null)
			//{
			//	return new byte[0];
			//}
			//byte[] buffer = new byte[directoryControlValue.Length];
			//for (int i = 0; i < directoryControlValue.Length; i++)
			//{
			//	buffer[i] = directoryControlValue[i];
			//}
			//return buffer;
		}
	}

	public struct STATS_2008
	{
		int threadCountTag;
		int threadCount;
		int callTimeTag;
		int callTime;
		int entriesReturnedTag;
		int entriesReturned;
		int entriesVisitedTag;
		int entriesVisited;
		int filterTag;
		string filter;
		int indexTag;
		string index;
		int pagesReferencedTag;
		int pagesReferenced;
		int pagesReadTag;
		int pagesRead;
		int pagesPrereadTag;
		int pagesPreread;
		int pagesDirtiedTag;
		int pagesDirtied;
		int pagesRedirtiedTag;
		int pagesRedirtied;
		int logRecordCountTag;
		int logRecordCount;
		int logRecordBytesTag;
		int logRecordBytes;
	}
}
